package junior.util.xml;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;

import junior.util.BeanUtils;
import junior.util.exception.ExceptionUtils;
import junior.util.reflect.ReflectUtils;
import junior.util.value.ValueUtils;
/**
 * 将Bean转换为XML，支持嵌套
 * @author WuBo
 * @date 2016年9月22日
 */
public class XmlMapper {
	private String encoding = "utf-8";
	private boolean writeXmlHead;
	private boolean wrapStringWithCdata;
	private boolean capitalizeField;
	private boolean uncapitalizeClass;
	private boolean indent;
	private boolean lineFeed;
	private Map<Class<?>, String> aliasMap = new HashMap<Class<?>, String>();
	private Map<Class<?>, Set<String>> ignoreMap = new HashMap<Class<?>, Set<String>>();
	private Map<Class<?>, Set<String>> ignoreTagMap = new HashMap<Class<?>, Set<String>>();
	private Set<Class<?>> uncapitalizeClassWhitelist = new HashSet<Class<?>>();
	private Set<Class<?>> uncapitalizeClassBlacklist = new HashSet<Class<?>>();
	private Map<Class<?>, Set<String>> capitalizeFieldWhitelist = new HashMap<Class<?>, Set<String>>();
	private Map<Class<?>, Set<String>> capitalizeFieldBlacklist = new HashMap<Class<?>, Set<String>>();

	public boolean isWriteXmlHead() {
		return writeXmlHead;
	}
	public XmlMapper setWriteXmlHead(boolean writeXmlHead) {
		this.writeXmlHead = writeXmlHead;
		return this;
	}
	public boolean isIndent() {
		return indent;
	}
	public XmlMapper setIndent(boolean indent) {
		this.indent = indent;
		return this;
	}
	public boolean isLineFeed() {
		return lineFeed;
	}
	public XmlMapper setLineFeed(boolean lineFeed) {
		this.lineFeed = lineFeed;
		return this;
	}
	public XmlMapper setPrettyFormat() {
		this.lineFeed = true;
		this.indent = true;
		return this;
	}
	public String getEncoding() {
		return encoding;
	}
	public XmlMapper setEncoding(String encoding) {
		this.encoding = encoding;
		return this;
	}
	/**
	 * String类型值是否用<![CDATA[xx]]>包围
	 * @return
	 */
	public boolean isWrapStringWithCdata() {
		return wrapStringWithCdata;
	}
	public XmlMapper setWrapStringWithCdata(boolean wrapStringWithCdata) {
		this.wrapStringWithCdata = wrapStringWithCdata;
		return this;
	}
	/**
	 * 是否字段名首字母大写
	 * @return
	 */
	public boolean isCapitalizeField() {
		return capitalizeField;
	}
	public XmlMapper setCapitalizeField(boolean capitalizeField) {
		this.capitalizeField = capitalizeField;
		return this;
	}
	/**
	 * 是否<b>取消</b>类名首字母大写
	 * @return
	 */
	public boolean isUncapitalizeClass() {
		return uncapitalizeClass;
	}
	public XmlMapper setUncapitalizeClass(boolean uncapitalizeClass) {
		this.uncapitalizeClass = uncapitalizeClass;
		return this;
	}
	public XmlMapper ignoreProperties(Class<?> clazz, String... ignoreProps) {
		for (String ignoreProp : ignoreProps)  {
			Set<String> ignoreSet = ignoreMap.get(clazz);
			if (ignoreSet == null) {
				ignoreSet = new HashSet<String>();
				ignoreMap.put(clazz, ignoreSet);
			}
			ignoreSet.add(ignoreProp);
		}
		return this;
	}

	public XmlMapper ignoreFieldTags(Class<?> clazz, String... ignoreFieldTags) {
		for (String ignoreFieldTag : ignoreFieldTags)  {
			Set<String> ignoreFieldTagSet = ignoreTagMap.get(clazz);
			if (ignoreFieldTagSet == null) {
				ignoreFieldTagSet = new HashSet<String>();
				ignoreTagMap.put(clazz, ignoreFieldTagSet);
			}
			ignoreFieldTagSet.add(ignoreFieldTag);
		}
		return this;
	}
	
	public XmlMapper alias(Class<?> clazz, String alias) {
		aliasMap.put(clazz, alias);
		return this;
	}

	public XmlMapper add2UncapitalizeClassWhiteList(Class<?> clazz) {
		uncapitalizeClassWhitelist.add(clazz);
		return this;
	}
	
	public XmlMapper add2UncapitalizeClassBlackList(Class<?> clazz) {
		uncapitalizeClassBlacklist.add(clazz);
		return this;
	}
	

	public XmlMapper add2CapitalizeFieldWhiteList(Class<?> clazz, String... fields) {
		for (String field : fields)  {
			Set<String> whiteList = capitalizeFieldWhitelist.get(clazz);
			if (whiteList == null) {
				whiteList = new HashSet<String>();
				capitalizeFieldWhitelist.put(clazz, whiteList);
			}
			whiteList.add(field);
		}
		return this;
	}
	
	public XmlMapper add2CapitalizeFieldBlackList(Class<?> clazz, String... fields) {
		for (String field : fields)  {
			Set<String> whiteList = capitalizeFieldBlacklist.get(clazz);
			if (whiteList == null) {
				whiteList = new HashSet<String>();
				capitalizeFieldBlacklist.put(clazz, whiteList);
			}
			whiteList.add(field);
		}
		return this;
	}

	public XmlMapper() {}
	
	public String bean2Xml(Object obj) {
		try {
			String xmlHead = "";
			if (isWriteXmlHead()) {
				xmlHead = getXmlHead();
			}
			return xmlHead + doBean2Xml(obj, 0);
		} catch (Exception e) {
			throw ExceptionUtils.wrap2Runtime(e);
		}
	}
	
	private String doBean2Xml(Object obj, int deep) throws Exception {
		StringBuilder xml = new StringBuilder();
		
		Class<?> clazz = obj.getClass();
		String alias = getAlias(obj);
		
		if (lineFeed) xml.append("\r\n");
		if (indent) xml.append(getIndent(deep));
		xml.append(getClassTagStart(clazz, alias));
		
		xml.append(getElements(obj, deep));

		if (lineFeed) xml.append("\r\n");
		if (indent) xml.append(getIndent(deep));
		xml.append(getClassTagEnd(clazz, alias));
		
		return xml.toString();
	}
	
	@SuppressWarnings("rawtypes")
	private String getElements(Object obj, int deep) throws Exception {
		StringBuilder xml = new StringBuilder();
		
		Class<?> clazz = obj.getClass();
		
		Method[] getters = ReflectUtils.getters4ExcludedNames(obj.getClass(), getIgnoreProps(obj));
		Arrays.sort(getters, new Comparator<Method>() {
			@Override
			public int compare(Method m1, Method m2) {
				Order o1 = m1.getAnnotation(Order.class);
				Order o2 = m2.getAnnotation(Order.class);
				if (o1 != null) {
					if (o2 != null) {
						return o1.value() < o2.value() ? -1 : 1;
					}
					return -1;
				}
				if (o2 != null) {
					return 1;
				}
				return 0;
			}
		});
		
		int sdeep = deep + 1;
		
		for (Method getter : getters) {
			String name = StringUtils.uncapitalize(getter.getName().substring(3));
			
			int eleDeep = sdeep;

			boolean isIgnoreFieldTag = isIgnoreFieldTag(clazz, name);
			if (! isIgnoreFieldTag) {
				if (lineFeed) xml.append("\r\n");
				if (indent) xml.append(getIndent(sdeep));
				xml.append(getTagStart(clazz, name));
				eleDeep =  sdeep + 1;
			}
			
			Object value = getter.invoke(obj);
			
			if (value != null) {
				if (String.class.isAssignableFrom(value.getClass())) {
					if (isWrapStringWithCdata()) xml.append("<![CDATA[");
					xml.append(value);
					if (isWrapStringWithCdata()) xml.append("]]>");
				} else if (BeanUtils.isNotDefinedBean(value.getClass())) {
					if (value.getClass().isArray()) {
						for (int i=0; i<Array.getLength(value); i++) {
							xml.append(doBean2Xml(Array.get(value, i), eleDeep));
						}
						if (! isIgnoreFieldTag) {
							if (lineFeed) xml.append("\r\n");
							if (indent) xml.append(getIndent(sdeep));
						}
					} else if(Collection.class.isAssignableFrom(value.getClass())) {
						for (Object ele : (Collection) value) {
							xml.append(doBean2Xml(ele, eleDeep));
						}
						if (! isIgnoreFieldTag){
							if (lineFeed) xml.append("\r\n");
							if (indent) xml.append(getIndent(sdeep));
						}
					} else {
						xml.append(ValueUtils.convert(value, String.class));
					}
				} else {
					xml.append(getElements(value, eleDeep));
					if (lineFeed) xml.append("\r\n");
					if (indent) xml.append(getIndent(sdeep));
				}
			}

			if (! isIgnoreFieldTag) {
				xml.append(getTagEnd(clazz, name));
			}
		}
		
		return xml.toString();
	}
	
	private String[] getIgnoreProps(Object obj) {
		Set<String> ignoreSet = ignoreMap.get(obj.getClass());
		return ignoreSet != null ? ignoreSet.toArray(new String[ignoreSet.size()]) : null;
	}
	
	private boolean isIgnoreFieldTag(Class<?> clazz, String field) {
		Set<String> set = ignoreTagMap.get(clazz);
		if (set != null) return set.contains(field);
		return false;
	}
	
	private String getAlias(Object obj) {
		String alias = aliasMap.get(obj.getClass());
		if (alias != null)  return alias;
		return obj.getClass().getSimpleName();
	}
	
	private String getIndent(int deep) {
		String[] str = new String[deep * 2];
		Arrays.fill(str, " ");
		return StringUtils.join(str);
	}
	
	private String getClassTagStart(Class<?> clazz, String className) {
		if (isUncapitalizeClass(clazz)) return "<"+StringUtils.uncapitalize(className)+">";
		return "<"+className+">";
	}
	
	private String getClassTagEnd(Class<?> clazz, String className) {
		if (isUncapitalizeClass(clazz)) return "</"+StringUtils.uncapitalize(className)+">";
		return "</"+className+">";
	}
	
	private String getTagStart(Class<?> clazz, String fieldName) {
		if (isCapitalizeField(clazz, fieldName)) return "<"+StringUtils.capitalize(fieldName)+">";
		return "<"+fieldName+">";
	}

	private String getTagEnd(Class<?> clazz, String fieldName) {
		if (isCapitalizeField(clazz, fieldName)) return "</"+StringUtils.capitalize(fieldName)+">";
		return "</"+fieldName+">";
	}
	
	private boolean isUncapitalizeClass(Class<?> clazz) {
		if (uncapitalizeClassWhitelist.contains(clazz)) return true;
		if (uncapitalizeClassBlacklist.contains(clazz)) return false;
		return uncapitalizeClass;
	}
	
	private boolean isCapitalizeField(Class<?> clazz, String field) {
		Set<String> whiteList = capitalizeFieldWhitelist.get(clazz);
		if (whiteList != null && whiteList.contains(field)) return true;
		
		Set<String> blackList = capitalizeFieldBlacklist.get(clazz);
		if (blackList != null && blackList.contains(field)) return false;
		
		return capitalizeField;
	}
	
	private String getXmlHead() {
		return "<?xml version=\"1.0\" encoding=\""+encoding+"\"?>";
	}

}
